Frame、Window、网络请求、存储
7 Frame 和 windw
7.1 弹窗和 window 的方法
弹窗(popup)就是打开一个给定 URL 的新窗口。大多数浏览器都配置为在新选项卡中打开 URL,而不是一个单独的窗口。
window.open("https://www.xxx.com/")
目前,进行OAuth 授权(使用 Google/Facebook/ ...登录),仍要使用弹窗,因为:
- 弹窗是一个独立的窗口,有自己独立的 JavaScript 环境。因此为了安全起见,常用弹窗方式打开一个不信任的第三方网站。
- 打开弹窗的代码很容易实现。
7.1.1 阻止弹窗
如果弹窗是在用户触发的事件处理程序(如 onclick
)之外调用,大多数浏览器都会默认阻止此类弹窗。
window.open('https://javascript.info'); // 弹窗被阻止
button.onclick = () => { // 弹窗被允许
window.open('https://javascript.info');
};
7.1.2 打开弹窗
window.open(url, name, params)
- url:要在新窗口中加载的 URL。
- name:新窗口的名称。每个窗口都有一个
window.name
,指定哪个窗口用于打开URL网页。- 如果已经有 name 名字的窗口,将在该窗口打开给定的 URL;否则会打开一个新窗口。
- params:新窗口的配置字符串。它包括设置,用逗号分隔。参数之间不能有空格,
- 例如:
width=200,height=100
。
- 例如:
params
的设置项:
- 位置:
left/top
(数字):屏幕上窗口的左上角的坐标。这有一个限制:不能将新窗口置于屏幕外(offscreen)。width/height
(数字):新窗口的宽度和高度。宽度/高度的最小值是有限制的,因此不可能创建一个不可见的窗口。
- 窗口功能:
menubar
(yes/no):显示或隐藏新窗口的浏览器菜单。toolbar
(yes/no):显示或隐藏新窗口的浏览器导航栏(后退,前进,重新加载等)。location
(yes/no):显示或隐藏新窗口的 URL 字段。Firefox 和 IE 浏览器不允许默认隐藏它。status
(yes/no):显示或隐藏状态栏。同样,大多数浏览器都强制显示它。resizable
(yes/no):允许禁用新窗口大小调整。不建议使用。scrollbars
(yes/no):允许禁用新窗口的滚动条。不建议使用。
7.1.3 操纵/关闭弹窗
操作弹窗
open
调用会返回对新窗口的引用。它可以用来操纵弹窗的属性,更改位置,甚至更多操作。
- 同源策略:只有在窗口是同源的时,窗口才能自由访问彼此的内容(
相同的协议://domain:port
)。
关闭弹窗
关闭一个窗口:win.close()
。
检查一个窗口是否被关闭:win.closed
。true 则表明已经被关闭
调整弹窗
win.moveBy(x,y)
:将窗口相对于当前位置向右移动x
像素,向下移动y
像素。- 允许负值(向上/向左移动)。
win.moveTo(x,y)
:将窗口移动到屏幕上的坐标(x,y)
处。win.resizeBy(width,height)
:根据给定的相对于当前大小的width/height
调整窗口大小。允许负值。win.resizeTo(width,height)
:将窗口调整为给定的大小。window.onresize
事件:当窗口尺寸发生改变,触发该事件。
没有最小化/最大化。无法通过 JavaScript 最小化、最大化个窗口。
滚动窗口
win.scrollBy(x,y)
:相对于当前位置,将窗口向右滚动x
像素,并向下滚动y
像素。允许负值。win.scrollTo(x,y)
:将窗口滚动到给定坐标(x,y)
。elem.scrollIntoView(top = true)
:滚动窗口,使elem
显示在elem.scrollIntoView(false)
的顶部(默认)或底部。window.onscroll
事件:窗口发生滚动时触发。
聚焦/失焦
window.focus()
和 window.blur()
方法可以使窗口获得或失去焦点。
在实际中它们被进行了严格地限制,因为在过去,恶意网站滥用这些方法。
7.2 跨窗口通信
“同源(Same Origin)”策略限制了窗口(window)和 frame 之间的相互访问。目的是为了提高安全性。
7.2.1 同源
如果两个 URL 具有相同的协议,域和端口,则称它们是“同源”的。
以下的几个 URL 都是同源的:
http://site.com
http://site.com/
http://site.com/my/page.html
但是下面这几个不是:
http://**www.**site.com
(另一个域:www.
影响)http://**site.org**
(另一个域:.org
影响)**https://**site.com
(另一个协议:https
)http://site.com:**8080**
(另一个端口:8080
)
“同源”策略规定:
- 如果我们有对另外一个窗口(例如,一个使用
window.open
创建的弹窗,或者一个窗口中的 iframe)的引用,并且该窗口是同源的,那么我们就具有对该窗口的全部访问权限。 - 否则,如果该窗口不是同源的,那么我们就无法访问该窗口中的内容:变量,文档,任何东西。
- 唯一的例外是
location
:我们可以修改它(进而重定向用户)。但无法读取location
(因此,我们无法看到用户当前所处的位置,也就不会泄漏任何信息)。
- 唯一的例外是
7.2.2 iframe
一个 <iframe>
标签承载了一个单独的嵌入的窗口,它具有自己的 document
和 window
。
访问属性:
iframe.contentWindow
:获取<iframe>
中的 window。iframe.contentDocument
:获取<iframe>
中的 document,- 是
iframe.contentWindow.document
的简写形式。
- 是
当访问嵌入的窗口中的变量时,浏览器会检查 iframe 是否具有相同的源。如果不是,则拒绝访问,仅允许:
- 对
location
进行写入; - 通过
iframe.contentWindow
获取对内部 window 的引用。
7.2.3 子域 - document.domain
根据定义,两个具有不同域的 URL 具有不同的源。
但是,如果窗口的二级域相同,例如 john.site.com
,peter.site.com
和 site.com
(它们共同的二级域是 site.com
),可以使浏览器忽略差异,使它们作为“同源”的来对待,进行跨窗口通信。
为了做到这一点,每个这样的窗口都执行下面的代码:
document.domain = 'site.com';
7.2.4 集合 - window.frames
SomeWindow.frames
:保存了该窗口下的全部 <iframe>
的 window 对象。
- 通过索引获取:
SomeWindow.frames[0]
:文档中的第一个 iframe 的 window 对象。 - 通过名称获取:
SomeWindow.frames.iframeName
:获取name="iframeName"
的 window 对象。
<iframe src="/" style="height:80px" name="win" id="iframe"></iframe>
<script>
alert(iframe.contentWindow == frames[0]); // true
alert(iframe.contentWindow == frames.win); // true
</script>
一个 iframe 内可能嵌套了其他的 iframe。相应的 window
对象会形成一个层次结构(hierarchy)。
可以通过以下方式获取:
SomeWindow.frames
—— “子”窗口的集合(用于嵌套的 iframe)。SomeWindow.parent
—— 对“父”(外部)窗口的引用。SomeWindow.top
—— 对最顶级父窗口的引用。
SomeWindow.frames[0].parent === window; // true
7.2.5 sandbox - iframe 特性
sandbox
特性(attribute)允许在 <iframe>
中禁止某些行为,防止执行不被信任的代码。
- 它通过将 iframe 视为非同源的,或者应用其他限制来实现 iframe 的“沙盒化”。
一个空的 "sandbox"
特性会施加最严格的限制。用一个以空格分隔的列表,列出要移除的限制。
<iframe sandbox="allow-forms allow-popups">
allow-same-origin
:默认情况下,"sandbox"
会为 iframe 强制实施“不同来源”的策略。- 它使浏览器将
iframe
视为来自另一个源,即使其src
指向的是同一个网站也是如此。
- 它使浏览器将
allow-top-navigation
:允许iframe
更改parent.location
。allow-forms
:允许在iframe
中提交表单。allow-scripts
:允许在iframe
中运行脚本。allow-popups
:允许在iframe
中使用window.open
打开弹窗。- 查看 官方手册 获取更多内容。
7.2.6 跨窗口通信
postMessage
接口允许两个具有任何源的窗口之间进行通信。
因此,这是解决“同源”策略的方式之一。它允许来自于 john-smith.com
的窗口与来自于 gmail.com
的窗口进行通信,并交换信息,但前提是它们双方必须均同意并调用相应的 JavaScript 函数。这可以保护用户的安全。
postMessage
发送方必须调用窗口的 postMessage 方法。
想把消息发送给
targetWin
,就要调用:targetWin.postMessage(data, targetOrigin)
。data:要发送的数据。
- 可以是任何对象,数据会被 “结构化序列化算法(structured serialization algorithm)”进行克隆。
- IE 浏览器只支持字符串,因此需要用
JSON.stringify
方法进行处理,以支持该浏览器。
targetOrigin:指定目标窗口的源,只有目标源窗口发送的信息,才能获得。
- 这是一种安全措施。
- 如果我们不希望做这个检查,可以将
targetOrigin
设置为*
。
onmessage
为了接收消息,目标窗口应该在 message
事件上有一个处理程序。
- 当
postMessage
被调用时触发该事件,并且targetOrigin
检查成功。
调用:window.addEventListener("message", function(event){ ... };
data
:从postMessage
传递来的数据。origin
:发送方的源,例如http://site.com
。source
:对发送方窗口的引用。方便立即用source.postMessage(...)
发送回信息。
要为 message
事件分配处理程序,我们应该使用 addEventListener
。 window.onmessage
不起作用。
8 网络请求
每次我们打开一个网页时,浏览器都会(使用 HTTP HTTPS协议)发送网络请求,请求 HTML 文档,也请求该文档依赖的图片、字体、脚本和样式表等等资源。浏览器除了能根据用户操作发送网络请求,也提供相关的 JavaScript API。
本章介绍 3 个网络 API:
- 基于 Promise 的
fetch
方法。可以发送 HTTP 和 HTTPS 请求。fetch()
API 几乎满足所有 HTTP 用例。fetch() API
取代了过时的XMLHttpRequest API
。
- SSE(Server-Send Event,服务器发送事件)。是为 HTTP “轮询” 技术提供的基于事件的一系列 API 接口。让 Web 服务器一直保持监听状态,随时向客户端发送数据。
- WebSocket。是一个网络协议,不是基于 HTTP 但在设计时考虑了与 HTTP 交互。它定义了一个异步消息传递 API,即客户端和服务器可以通过与 TCP 网络套接口类似的方式相互发送和接收信息。
8.1 Fetch
JavaScript 可以将网络请求从用户页面端发送到服务器,并在需要时加载新信息到用户端。
例如,我们可以使用网络请求来:
- 提交订单,
- 加载用户信息,
- 从服务器接收最新的更新,
- ……等。
加载新信息不需要完整的重新加载页面。
对于来自 JavaScript 的网络请求,有一个总称术语 “AJAX”(Asynchronous JavaScript And XML 的简称)。
- 我们不必使用 XML 技术:这个术语诞生于很久以前,所以 XML 虽然推出历史舞台,但名称未变。
基本语法:
let promise = fetch(url, [options])
url
:要访问的 URL。options
:可选参数:method,header 等。
一个普通的 GET 请求,没有 option
参数,就会下载 url
的内容。
具体流程是这样的:
浏览器立即启动请求,并返回等待返回的
promise
,里面保存着获取的结果。获取响应的第一阶段:
- 服务器向用户端发送了响应头(response header),在一切都成功的情况下,客户端会收到该信息。
- 客户端的
fetch()
得到一个已解决的 promise,这就是一个 Response 对象。 - 此时我们得到了一个 response header,通过调用一些方法,我们可以继续请求
response body
。
获取响应的第二阶段:
- 服务器向用户端发送了响应体(response body),在一切都成功的情况下,客户端会收到该信息。
我们可以进行:
- 通过检查响应头,来判断 HTTP 状态以确定请求是否成功。
- 如果
fetch
无法建立一个 HTTP 请求。如网络问题,请求网址不存在,则 promise 会 reject。 - 如果 HTTP 状态异常,则不会 reject,而是会返回状态码,如 404,500。 response 属性查看:
response.status
:HTTP 状态码,例如 200。response.ok
:布尔值,如果 HTTP 状态码为 200-299,则为true
。
获取响应的第二阶段:
- 确定连接成功,开始获取响应体(response body)
Response
提供了多种基于 promise 的方法,来以不同的格式访问 body:response.text()
:读取 response,并以文本形式返回 response,response.json()
:将 response 解析为 JSON,response.formData()
:以FormData
对象( 这里解释 )的形式返回 response,response.blob()
:以 Blob(具有类型的二进制数据)形式返回 response,response.arrayBuffer()
:以 ArrayBuffer(低级别的二进制数据)形式返回 response,response.body
: ReadableStream 对象,它允许逐块读取 body。
举例,从 GitHub 获取最新 commits 的 JSON 对象:
- 也就是说:发送一个
fetch
后,返回的第一个promise
通常称之为response
。
// 使用 Promise
fetch('https://api.github.com/repos/javascript-tutorial/en.javascript.info/commits')
.then(response => response.json())
.then(commits => alert(commits[0].author.login));
// 使用 await
let url = 'https://api.github.com/repos/javascript-tutorial/en.javascript.info/commits';
let response = await fetch(url);
let commits = await response.json(); // 读取 response body,并将其解析为 JSON
alert(commits[0].author.login);
- 我选择一种读取 body 的方法。
- 如果已经使用了
response.text()
方法来获取 response。再用response.json()
,则不会生效,因为 body 内容已经被处理过了。
Response header 响应 (服务端->客户端)
在 response.headers
指向一个类 Map 的 header 对象。
虽然不是 Map,但具有类似的方法,可以按名称 name 迭代 header:
let response = await fetch('https://api.github.com/repos/javascript-tutorial/en.javascript.info/commits');
// 获取一个 header
header2 = response.headers.get('Content-Type'));
console.log(header2); // application/json; charset=utf-8
// 迭代所有 header
for (let [key, value] of response.headers) {
console.log(`${key} = ${value}`);
}
Request header 请求 (客户端->服务端)
在向服务端发送 fetch
时,可设置具体的 Request header,在 headers
可选参数中设置:
let response = fetch(protectedUrl, {
headers: {
Authentication: 'secret'
// ...
}
});
这里有非常多的属性,有些可以设置,有些不可以,具体属性不在此详赘了。
8.2 WebSocket
8.3 Server Sent Events
9 存储
使用浏览器 API,可以在用户计算机上本地存储数据。客户端存储的目的是让浏览器能够记住一些信息。
- 比如:存储对网页设置的用户偏好,存储页面完成状态以便恢复上次离开时的情景等等。
客户端存储有两个要点:
- 数据的访问权限:数据是按照来源隔离的。因此来自 A 站的的页面不能读取来自 B 站的页面存储的数据。
- 数据的生命周期:
- 可以临时存储(关闭页面就删除数据、时钟到期就删除数据)
- 可以长期存储(存储在用户计算机硬盘中)
客户端存储分为三种主要的形式:
- Web Storage:分为
localStorage
和sessionStorage
对象。是常用的存储方式。 - Cookie:一种古老的存储方式。保存在 Cookie 中的数据也会随 HTTP 请求发送给服务器,只能存极少的数据
- IndexedDB:一个支持索引的对象数据库。
9.1 localStorage和
sessionStorage
Window 对象的 localStorage
和 sessionStorage
的属性,引用的是 Storage 对象。 Storage 对象与普通 JavaScript 对象非常相似,不同之处:
- Storage 对象的属性值必须是字符串;
- Storage 对象中存储的属性是持久化的。
可用的操作:
delete
:删除 localStorage
和 sessionStorage
的中属性;
for .. in
:遍历 localStorage
和 sessionStorage
的中属性;
localStorage.clear()
:删除 storage 对象的所有属性。
读写属性、删除属性:
getItem()
setItem()
deleteItem()
生命期和作用域
生命期:
localStorage
:数据是永久性的,除非 Web 应用或用户通过浏览器(特定的界面)删除,否则数据会永远保存在用户设备上。sessionStorage
:数据的生命期与存储它的脚本所属的顶级窗口或浏览器标签页相同。窗口或标签页关闭后,sessionStorage
中存储的数据也都会被删除。
作用域:
localStorage
:作用域为文档来源。文档来源由协议、域名、端口共同定义。所有同源文档都共享相同的 localstorage 数据。sessionStorage
:作用域为文档来源 + 窗口隔离。如果用户在两个浏览器标签页中打开了同一来源的文档,这两个标签页的sessionStorage
数据也是隔离的。
存储事件
window.onstorage
事件:存储在 localStorage
中的数据每当发生变化,在数据可见的 Window 对象上就会触发该事件。也就是说,如果在作用域内的所有 window 窗口(即使是不同的标签页),都可以监听到数据的变化。
localStorage
和 storage
事件可以作为一种广播机制,即浏览器向所有当前浏览同一网站的窗口发送信息。
9.2 cookie
cookie 是浏览器为特定网页或网站保存的少量命名数据。 cookie 是为服务器编程而设计的,在最底层上作为 HTTP 协议的扩展实现。cookie 数据会自动在浏览器与 Web 服务器之间传输,因此服务器端脚本可以读写存储在客户端的 cookie 值。
读取 cookie
document.cookie
属性返回一个包含当前文档有关的所有 cookie的字符串。字符串是一个分号和空格分隔的 K/V 对。cookie 的值就是 Value。
- 通常调用
split()
方法把整个字符串拆分成一个个 K/V 对。
生命期和作用域
cookie 有一个可选的属性,可以控制其生命期和作用域。
生命期:
- 默认:生命期很短,只在浏览器会话期间存在,用户退出浏览器后,就会丢失。
- 指定生命期:以秒为单位,时间到期后会删除数据。如果指定了时间长的生命期,浏览器就会把 cookie 存储在本地文件中,等时间到了再把它们删除。
作用域:为文档来源 + 文档路径。
- 文档来源(
domain
属性):同 localStorage 效果。 - 文档路径(
path
属性):默认情况下, cookie 的可访问权限为 该网页位于相同目录和子目录下的其他网页(即,兄弟网页、子网页、兄弟的子网页)。- 也可以指定 path 路径。这样来自同一服务器的任何网页,都可以访问一个 cookie。
安全性:secure
- 是一个布尔值,默认情况下是不安全的。如果设置为安全的,则浏览器和服务器通过 HTTPS 或其他安全协议连接,才可以传输 cookie。
9.3 IndexedDB
IndexedDB 是一个简单的对象数据库。可以通过 JavaScript API 在用户计算机上持久存储 JavaScript 对象。
- 作用域:文档来源。
- 生命期:在计算机上持续存储。
IndexedDB 提供了原子保证,有完整的事务处理机制。
IndexedDB 数据库的名字必须在当前来源下唯一。存储的是对象,会使用结构化克隆算法序列化为一个对象存储。每个对象必须有一个键,可以用于排序和从存储中检索。键必须唯一。
- JavaScript 字符串、数值、Date 对象都是有效的键。也可以自动为插入数据库中的每个对象生成唯一的键。
查找数据
- 可以按对象的某个属性查找
- 可以按索引查找
- 可以按唯一的键查找
更新数据库
IndexedDB 是异步的,且该 API 是在 Promise 得到广泛支持之前定义的,因此这个 API 是基于事件而不是 Promise。